/*
 * Flashlib - QB9 common Flash library
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.qb9.flashlib.input.reader
{
	import com.qb9.flashlib.interfaces.IDisposable;
	import com.qb9.flashlib.math.QMath;
	import com.qb9.flashlib.utils.StringUtil;

	public class StringReader implements IDisposable
	{
		public static const ALPHA:RegExp = /\w/;
		public static const NUMBER:RegExp = /\d/;
		
		public static const CARRIAGE_RETURN:RegExp = /\r/g;
		public static const WHITE_SPACE:RegExp = /\s/m;
		public static const WORD_SEPARATOR:RegExp = /[\n ,.;:-]/;
		
		public static const NEW_LINE:String = '\n';
		public static const EMPTY:String = '';
		public static const SPACE:String = ' ';
		public static const DOT:String = '.';
		public static const MINUS:String = '-';
		
		protected var source:String;
		protected var _pointer:uint;
		
		public function StringReader(source:String=null)
		{
			if (source)
				setSource(source);
		}
		
		protected function clean(source:String):String
		{
			return source.replace(CARRIAGE_RETURN, EMPTY);
		}
		
		public function setSource(source:String):void
		{
			this.source = clean(source);
			reset();
		}
		
		public function reset():void
		{
			_pointer = 0;
		}
		
		public function take(amount:int=1):String
		{
			var str:String = peek(amount);
			step(amount);
			return str;
		}
		
		public function peek(amount:int=1):String
		{
			return source.substr(pointer, amount);
		}
		
		protected function at(i:int):String
		{
			return source.charAt(i);
		}
		
		protected function step(amount:int=1):void
		{
			_pointer = QMath.clamp(pointer + amount, 0, bytesTotal);
		}
		
		public function get pointer():uint
		{
			return _pointer;
		}

		public function get bytesAvailable():uint
		{
			return bytesTotal - pointer;
		}
		
		public function get bytesTotal():uint
		{
			return source.length;
		}
		
		// Base
		
		protected function matches(str:String, token:Object):Boolean
		{
			return token is RegExp ? RegExp(token).test(str) : str === token;
		}
		
		protected function baseRead(token:Object, expected:Boolean):String
		{
			for (var amount:uint = 0, max:int = bytesAvailable; amount < max; amount++)
			{
				var char:String = at(amount+pointer);
				if (matches(char, token) !== expected)
					break;
			}

			return amount ? take(amount) : null; 
		}
		
		// Generic
		
		public function readWhile(token:Object):String
		{
			return baseRead(token, true);
		}
		
		public function readUntil(token:Object):String
		{
			return baseRead(token, false);
		}
		
		protected function tokenLength(token:Object):uint
		{
			return token is RegExp ? 1 : token.toString().length;
		}
		
		public function read(token:Object):String
		{
			var amount:uint = tokenLength(token);
			var c:String = peek(amount);
			if (!matches(c, token))
				return null;

			step(amount);
			return c;
		}
		
		// Single
		
		public function readSpace():String
		{
			return read(SPACE);
		}
		
		public function readNewLine():String
		{
			return read(NEW_LINE);
		}
		
		// Groups
		
		public function readAlpha():String
		{
			return readWhile(ALPHA);
		}
 
		public function readWord():String
		{
			// Using readWhile(ALPHA) would stop on ñ and accented characters
			return readUntil(WORD_SEPARATOR);
		}
 
 		public function readUnsignedInt():String
		{
			return readWhile(NUMBER);
		}
 
 		public function readInt():String
		{
			var c:String = read(MINUS) ? MINUS : EMPTY;
			return c + readUnsignedInt();
		}
 
		public function readNumber():String
		{
			var str:String = readInt();
			if (read(DOT))
				str += DOT + readUnsignedInt();
			return str;
		}

		public function readLine():String
		{
			var str:String = readUntil(NEW_LINE);
			step();
			return str;
		}
 
		// Assertions
		
		protected function get lineNumber():uint
		{
			return source.slice(0, pointer).split(NEW_LINE).length;
		}
		
		protected function error(...args:Array):void
		{
			var lines:Array = source.slice(0, pointer).split(NEW_LINE);
			// Last line or the previous if empty
			var line:String = StringUtil.trim(lines.pop()) || (lines.pop()+NEW_LINE);
			
			args.push('at: ', line, '<--');
			
			throw new StringReaderError(args.join(' '));
		}
		
		public function expect(token:Object):String
		{
			var c:String = read(token);
			if (!matches(c, token))
				error('expected "'+ token + '" but found "'+ peek(tokenLength(token)) + '"');
			return c;
		}
		
		public function expectSpace():String
		{
			return expect(SPACE);
		}
		
		public function expectNewLine():void
		{
			expect(NEW_LINE);
		}
		
		public function expectEnd():void
		{
			if (bytesAvailable > 0)
				error('expected empty buffer but found', bytesAvailable, 'bytes');
		}

		// Disposal
		
		public function dispose():void
		{
			source = null;
		}
		
	}
}